/**
 *
 */
package org.jprobe.instantiate;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.jprobe.JProbeException;
import org.jprobe.classpath.ClassPathInterface;
import org.jprobe.classpath.ClassPathReader;
import org.jprobe.codegen.CodeGenException;
import org.jprobe.codegen.CodeGenerator;
import org.jprobe.utils.IOUtils;
import org.jprobe.utils.JavaTools;
import org.jprobe.utils.RegExUtils;

/**
 * provides methods determining if class is intilaizable, and if so, gets
 * an initialized instance of an object
 * @author Hussain Al-Mutawa
 * @version 1.0
 * @since 1.0
 */
public class Instantiator implements InstantiatorInterface {
	/** creates a new instance of an initializer object **/
	public Instantiator() {super();}

	/**
	 * a map which contains the primitive types by their names, since JVM
	 * has special codes for primitive types, and therefore to avoid translating
	 * such codes, a straightforward way is holding a reference to each primitive
	 * class by its normal name
	 */
	private transient static final Map<String, Class<?>> DEFINED_CLASSES = new HashMap<String, Class<?>>();

	static {
		DEFINED_CLASSES.put("int", int.class);
		DEFINED_CLASSES.put("char", char.class);
		DEFINED_CLASSES.put("float", float.class);
		DEFINED_CLASSES.put("long", long.class);
		DEFINED_CLASSES.put("double", double.class);
		DEFINED_CLASSES.put("byte", byte.class);
		DEFINED_CLASSES.put("short", short.class);
		DEFINED_CLASSES.put("boolean", boolean.class);
		DEFINED_CLASSES.put("void", Void.TYPE);
	}

	/** classpath of the project **/
	private transient static final ClassPathInterface CLASSPATH=ClassPathReader.instance.getClassPath();


	@Override
	public boolean isInitializable(final Class<?>clazz) throws Exception {
		boolean result;

		if(clazz.isPrimitive() || clazz.isInterface() || clazz.isArray()){
			result=true;
		}else if(clazz.isEnum()){
			result=false;
		}else{
			result = getInitializedObject(clazz)!=null;//clazz.getConstructors().length>0 || (clazz.getDeclaredConstructors().length>0 && !CLASSPATH.isRuntimeClass(clazz));
		}
		return result;
	}

	@Override
	public Object getInitializedObject(final Class<?>clazz)throws Exception{
		final String project=getProjectDirectory();
		final String src = project+"/jprobe/src/";
		final String dst = project+"/jprobe/bin/";
		final String impl = CLASSPATH.isRuntimeClass(clazz)?"/impl/":"";
		final String path = src+(impl+(clazz.getPackage()==null?"":clazz.getPackage().getName().replaceAll("\\.", "/")))+"/"+clazz.getSimpleName().replaceAll("\\[\\]", "A")+"JProbe.java";
		//System.out.println(path);
		Method method;
		Class<?>iclazz=null;
		Object result;
		final File javaFile = new File(path);
		try{

			final String className = javaFile.getName().replace(".java","");
			final String packageName = RegExUtils.extract(IOUtils.readFromFileSys(javaFile.getAbsolutePath()),RegExUtils.PACKAGE,2).trim();
			final String cls = ("".equals(packageName)?"":packageName+'.')+className;
			iclazz = ClassPathReader.getClassloader().loadClass(cls);

		}catch(Throwable throwable){

			final Class<?>[] accepted = {
					ClassNotFoundException.class,
					IOException.class,
					FileNotFoundException.class
			};
			
			if(!Arrays.asList(accepted).contains(throwable.getClass())){
				throw new Exception(throwable);
			}
			
			new File(src).mkdirs();
			new File(dst).mkdirs();
			final String code=new CodeGenerator().getInstantiationCode(clazz);
			writeToFile(javaFile, code);
			iclazz = JavaTools.compile(new File(src), new File(dst), javaFile);
			ClassPathReader.instance.addClassPathUrlToSystemClassLoader(dst);

		}
		
		if(iclazz==null){
			throw new CodeGenException("iclazz is null");
		}
		
		method = iclazz.getDeclaredMethod("$",new Class<?>[]{});
		result = method.invoke(iclazz.newInstance(), new Object[]{});
		
		return result;
	}

	/**
	 * Writes the contents to the given file.
	 * if the directory is not available, it will be made
	 *
	 * @param file the file to write contents to it
	 * @param contents the text contents to be written to the file
	 * @throws IOException if the path to the file does not exits, or writing to the disk is not possible
	 */
	protected void writeToFile(final File file, final String contents) throws IOException{
		final File dir = file.getParentFile();

		if(dir==null || !(dir.exists() || dir.mkdirs())){
			throw new IOException("failed to write to file system / malformed file path");
		}

		final FileOutputStream fos = new FileOutputStream(file);
		final OutputStreamWriter outw = new OutputStreamWriter(fos);
		outw.write(contents);
		outw.close();
		fos.close();
	}

	/**
	 * gets the current project directory
	 * you may override this method to change the default
	 * output directory
	 * @return the project directory path
	 */
	public String getProjectDirectory() {
		return ".";
	}

	@Override
	public Class<?> loadClass(String className) throws ClassNotFoundException {
		Class<?>result;
		if(DEFINED_CLASSES.containsKey(className)){
			result = DEFINED_CLASSES.get(className);
		}else{
			result = Class.forName(className,false,getClass().getClassLoader());
		}
		return result;
	}


	@Override
	public Constructor<?> getFirstPublicConstructor(final Class<?> clazz) {
		Constructor<?>result=null;

		try{
			result=clazz.getConstructor(new Class<?>[]{});
		}catch(Exception exp){
			result=clazz.getConstructors().length>0?clazz.getConstructors()[0]:null;
		}

		if(result==null){
			final List<Class<?>>subclasses=CLASSPATH.getSubClasses(clazz);
			for(final Class<?>aclazz:subclasses){
				result=getFirstPublicConstructor(aclazz);
				if(!(result==null || isSelfParameterizedConstructor(result))){
					break;
				}
			}
		}

		return result;
	}

	@Override
	public boolean isSelfParameterizedConstructor(final Constructor<?>constructor){
		final Class<?>clazz=constructor.getDeclaringClass();
		final Class<?>[]types=constructor.getParameterTypes();
		for(final Class<?>type:types){
			if(type==clazz){
				return true;
			}else if(type.isArray()){
				Class<?>comp=type;
				do{comp=comp.getComponentType();}while(comp.isArray());
				if(comp==clazz){
					return true;
				}
			}
		}
		return false;
	}

	public Set<Constructor<?>> getAllConstructors(final Class<?>clazz){
		final SortedSet<Constructor<?>>constructors=new TreeSet<Constructor<?>>(ConstructorsComparator.instance());
		constructors.addAll(Arrays.asList(clazz.getConstructors()));
		final List<Class<?>>subclasses=CLASSPATH.getSubClasses(clazz);
		for(final Class<?>subclass:subclasses){
			constructors.addAll(getAllConstructors(subclass));
		}
		return constructors;
	}

	@Override
	public Collection<Constructor<?>> getPublicConstructors(Class<?> clazz) {
		final SortedSet<Constructor<?>>constructors=new TreeSet<Constructor<?>>(ConstructorsComparator.instance());
		constructors.addAll(Arrays.asList(clazz.getConstructors()));
		return constructors;
	}

	@Override
	public List<Method> getInitializers(final Class<?> initializeWhat,final Class<?> initializer,final char modifierType) {
		final List<Method> list = new LinkedList<Method>();

		for(final Method method:initializer.getMethods()){

			final int modifiers = method.getModifiers();
			final boolean static_modifier =
				((modifierType==STATIC_ONLY_MODIFIER && Modifier.isStatic(modifiers))
						^
						(modifierType==NON_STATIC_MODIFIER && !Modifier.isStatic(modifiers)))
						||
						modifierType==ANY_MODIFIER;
			if(Modifier.isPublic(modifiers) && static_modifier){
				final boolean test1 = method.getReturnType()==initializeWhat;
				final boolean test2 = CLASSPATH.getSuperClasses(initializeWhat,true,true).contains(method.getReturnType());
				final boolean test3 = CLASSPATH.getSuperClasses(method.getReturnType(),true,true).contains(initializeWhat);
				if(test1 || test2 || test3){
					list.add(method);
				}
			}
		}
		return list;
	}

	@Override
	public Object getPrivateEmptyConstructorByPassed(final Class<?>clazz) throws Exception {
		final Constructor<?>constructor = clazz.getDeclaredConstructor(new Class<?>[]{});
		constructor.setAccessible(true);
		return (Object) constructor.newInstance(getNullInitializedParameters(constructor));
	}


	@Override
	public Object[] getNullInitializedParameters(final AccessibleObject accessibleObject) throws Exception {
		final Class<?>[]types;
		if(accessibleObject.getClass()==Constructor.class) {
			types = ((Constructor<?>)accessibleObject).getParameterTypes();
		}else{
			types = ((Method)accessibleObject).getParameterTypes();
		}

		final List<Object>params=new LinkedList<Object>();

		for(final Class<?>type:types){
			Object value = null;
			try {
				value = getInitializedObject(type);
			} catch (JProbeException e) {
				throw new InstantiatorException(e);
			}
			params.add( value );
		}

		return params.toArray();
	}


	@Override
	public List<AccessibleObject> getClassInitilizers(final Class<?>clazz) throws ClassNotFoundException {
		final List<AccessibleObject> result = new LinkedList<AccessibleObject>();

		//list of static methods which return an initialized object
		result.addAll(getClassInitilizers(clazz.getPackage().getName(), clazz));

		//list of public constructors in the class
		result.addAll(getPublicConstructors(clazz));

		return result;
	}

	@Override
	public Set<AccessibleObject> getClassInitilizers(final String pckg, final Class<?>clazz) throws ClassNotFoundException{
		final Set<AccessibleObject> result = new HashSet<AccessibleObject>();

		//list of static methods which return an initialized object
		for(final Class<?>cls:CLASSPATH.getClasses(pckg)){
			final List<Method>list=getInitializers(clazz, cls, ANY_MODIFIER);
			result.addAll(list);
		}

		return result;
	}

	@Override
	public Object[] getParametersValues(final Class<?>[] parameterTypes) throws Exception{
		final Object[] result = new Object[parameterTypes.length];
		for(int i=0;i<result.length;i++){
			result[i] = getInitializedObject(parameterTypes[i]);
		}
		return result;
	}


	@Override
	public List<Method> getFactoryCreatorMethod(final Class<?> clazz) {
		return getInitializers(clazz,clazz, STATIC_ONLY_MODIFIER);
	}

	@Override
	public boolean hasPublicEmptyConstructor(final Class<?> clazz) {
		boolean result=false;
		try{
			result = clazz.getConstructor(new Class<?>[]{})!=null;
		}catch(Exception e){}
		return result;
	}

	@Override
	public boolean hasNoPublicConstructors(final Class<?> clazz) {
		return getAllConstructors(clazz).isEmpty();
	}
}
